﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace Safebrowsing
{
    public class Utils
    {
        public const int LF = '\n';
        private const int BUFFER_SIZE = 4096;
        private static readonly SHA256 SHA256_INSTANCE = SHA256.Create();

        /// <summary>
        /// Returns ISO-8859-1 encoding, which is default for HTTP headers
        /// </summary>
        public static Encoding DefaultEncoding
        {
            get { return Encoding.GetEncoding("ISO-8859-1"); }
        }

        /// <summary>
        /// Joins array of integers
        /// </summary>
        /// <param name="delimiter"></param>
        /// <param name="array"></param>
        /// <returns></returns>
        public static string Join(char delimiter, IEnumerable<int> array)
        {
            var sb = new StringBuilder();

            IEnumerator<int> en = array.GetEnumerator();

            while (en.MoveNext())
            {
                sb.Append(delimiter);
                sb.Append(en.Current);
            }

            return sb.Remove(0, 1).ToString();
        }

        /// <summary>
        /// Joins array of integers into list of ranges.
        /// LIST = (RANGE | NUMBER) ["," LIST]
        /// NUMBER = DIGIT+
        /// RANGE = NUMBER "-" NUMBER
        /// </summary>
        /// <param name="array"></param>
        /// <returns></returns>
        public static string JoinRangeList(IEnumerable<int> array)
        {
            List<int> sorted = array is List<int> ? (List<int>) array : new List<int>(array);
            if (sorted.Count == 0) return string.Empty;
            if (sorted.Count == 1) return sorted[0].ToString();

            sorted.Sort();

            var sb = new StringBuilder();
            int rangeStart = sorted[0];
            int rangeEnd = sorted[0];

            for (int i = 1; i < sorted.Count; i++)
            {
                int val = sorted[i];

                if ((val - rangeEnd) == 1)
                {
                    rangeEnd = val;
                }
                else
                {
                    if (rangeStart < rangeEnd)
                    {
                        sb.Append(rangeStart);
                        sb.Append("-");
                        sb.Append(rangeEnd);
                        sb.Append(",");
                    }
                    else
                    {
                        sb.Append(rangeStart);
                        sb.Append(",");
                    }

                    rangeStart = val;
                    rangeEnd = val;
                }
            }

            sb.Append(rangeStart);
            if (rangeStart < rangeEnd)
            {
                sb.Append("-");
                sb.Append(rangeEnd);
            }
            return sb.ToString();
        }

        /// <summary>
        /// Splits list of ranges.
        /// LIST = (RANGE | NUMBER) ["," LIST]
        /// NUMBER = DIGIT+
        /// RANGE = NUMBER "-" NUMBER
        /// </summary>
        /// <param name="array"></param>
        /// <returns></returns>
        public static List<int> SplitRangeList(string array)
        {
            var list = new List<int>();
            string[] parts = array.Split(',');
            foreach (string part in parts)
            {
                if (part.Contains("-"))
                {
                    string[] range = part.Split('-');
                    if (range.Length == 2)
                    {
                        int startRange = ToInt(range[0]);
                        int endRange = ToInt(range[1]);
                        if (startRange >= 0 && endRange >= 0)
                        {
                            for (int i = Math.Min(startRange, endRange); i <= Math.Max(startRange, endRange); i++)
                            {
                                if (!list.Contains(i)) list.Add(i);
                            }
                        }
                    }
                }
                else
                {
                    int value = ToInt(part);
                    if (value >= 0 && !list.Contains(value)) list.Add(value);
                }
            }

            return list;
        }

        /// <summary>
        /// To form a host key, first canonicalize the url then take the three most significant host components if there are three or 
        /// more components in the blacklist entry, or two host components if a third does not exist. Append a trailing slash (/) to this string, 
        /// then use the 32 most significant bits of the string's hash as the host key. To be clear, to match a URL against a host key, a 
        /// client must try matching based upon the two most significant host components, and also the three most significant host components 
        /// if three such components exist.
        /// 
        /// An exception to the above exists when the host is an IP address. Here, we form the host key by taking the SHAVAR hash of 
        /// the entire host (IP address), NOT two or three octets of the IP address.
        /// </summary>
        /// <param name="uri"></param>
        /// <returns></returns>
        public static List<int> GetHostKeys(Uri uri)
        {
            string hostKey = uri.GetComponents(UriComponents.Host, UriFormat.UriEscaped) + "/";

            if (uri.HostNameType == UriHostNameType.IPv4 || uri.HostNameType == UriHostNameType.IPv6)
            {
                return new List<int> { HostKeyToInt(hostKey) };
            }

            string[] parts = hostKey.Split('.');

            if (parts.Length <= 2) return new List<int> {HostKeyToInt(hostKey)};

            return new List<int> { HostKeyToInt(string.Join(".", parts, parts.Length - 3, 3)), HostKeyToInt(string.Join(".", parts, parts.Length - 2, 2)) };
        }

        private static int HostKeyToInt(string hostKey)
        {
            byte[] hash = SHA256_INSTANCE.ComputeHash(DefaultEncoding.GetBytes(hostKey));
            return ToInt(hash, 0);
        }

        /// <summary>
        /// To perform a lookup for a given url, the client will try to form different possible host suffix and path prefix combinations 
        /// and seeing if they match each list. Depending on the list type, the suffix/prefix combination may be hashed before lookup. 
        /// For these lookups, only the host and path components of the URL are used. The scheme, username, password, and port are disregarded. 
        /// If query parameters are present in the url, the client will also include a lookup with the full path and query parameters.
        /// </summary>
        /// <param name="uri"></param>
        /// <param name="maxHostPrefixes"></param>
        /// <param name="maxPathSuffixes"></param>
        /// <returns></returns>
        public static List<string> GetSuffixPrefixExpressions(Uri uri, int maxHostPrefixes, int maxPathSuffixes)
        {
            var expressions = new List<string>();

            List<string> hostPrefixes = GetHostPrefixComponents(uri, maxHostPrefixes);
            List<string> pathSuffixes = GetPathSuffixComponents(uri, maxPathSuffixes);

            foreach (string prefix in hostPrefixes)
            {
                foreach (string suffix in pathSuffixes)
                {
                    expressions.Add(prefix + suffix);
                }
            }

            return expressions;
        }

        /// <summary>
        /// To perform a lookup for a given url, the client will try to form different possible host suffix and path prefix combinations 
        /// and seeing if they match each list. Depending on the list type, the suffix/prefix combination may be hashed before lookup. 
        /// For these lookups, only the host and path components of the URL are used. The scheme, username, password, and port are disregarded. 
        /// If query parameters are present in the url, the client will also include a lookup with the full path and query parameters.
        /// </summary>
        /// <param name="uri"></param>
        /// <param name="maxHostPrefixes"></param>
        /// <param name="maxPathSuffixes"></param>
        /// <returns></returns>
        public static List<string> GetHashedSuffixPrefixExpressions(Uri uri, int maxHostPrefixes, int maxPathSuffixes)
        {
            var expressions = new List<string>();

            List<string> hostPrefixes = GetHostPrefixComponents(uri, maxHostPrefixes);
            List<string> pathSuffixes = GetPathSuffixComponents(uri, maxPathSuffixes);

            foreach (string prefix in hostPrefixes)
            {
                foreach (string suffix in pathSuffixes)
                {
                    expressions.Add(ComputeSha256Hash(prefix + suffix));
                }
            }

            return expressions;
        }

        /// <summary>
        /// For the hostname, the client will try at most 5 different strings. They are:<br/>
        /// the exact hostname in the url<br/>
        /// up to 4 hostnames formed by starting with the last 5 components and successively removing the leading component. The top-level domain <br/>
        /// can be skipped. These additional hostnames should not be checked if the host is an IP address.<br/>
        /// </summary>
        /// <param name="uri"></param>
        /// <param name="maxHostPrefixes"></param>
        /// <returns></returns>
        public static List<string> GetHostPrefixComponents(Uri uri, int maxHostPrefixes)
        {
            var prefixes = new List<string>();

            string host = uri.GetComponents(UriComponents.Host, UriFormat.UriEscaped);
            prefixes.Add(host);
            
            if (uri.HostNameType == UriHostNameType.IPv6 || uri.HostNameType == UriHostNameType.IPv4)
            {
                // Do not checking other host names for the ip address
                return prefixes;
            }

            string[] parts = host.Split('.');

            // 2nd level domain name
            if (parts.Length <= 2) return prefixes;

            int prefixesCount = (parts.Length > maxHostPrefixes + 2 ? maxHostPrefixes : parts.Length - 2);
            for (int i = 0; i < prefixesCount; i++)
            {
                prefixes.Add(string.Join(".", parts, parts.Length - 2 - i, i + 2));
            }

            return prefixes;
        }

        /// <summary>
        /// For the path, the client will also try at most 6 different strings. They are:<br/>
        /// the exact path of the url, including query parameters<br/>
        /// the exact path of the url, without query parameters<br/>
        /// the 4 paths formed by starting at the root (/) and successively appending path components, including a trailing slash.<br/>
        /// </summary>
        /// <param name="uri"></param>
        /// <param name="maxPathSuffixes"></param>
        /// <returns></returns>
        public static List<string> GetPathSuffixComponents(Uri uri, int maxPathSuffixes)
        {
            var suffixes = new List<string>();

            string pathAndQuery = uri.GetComponents(UriComponents.PathAndQuery, UriFormat.UriEscaped);
            string query = uri.GetComponents(UriComponents.Query, UriFormat.UriEscaped);
            string path = uri.GetComponents(UriComponents.Path, UriFormat.UriEscaped);

            if (!string.IsNullOrEmpty(pathAndQuery)) suffixes.Add(pathAndQuery);
            if (!string.IsNullOrEmpty(query)) suffixes.Add("/" + path);

            if (string.IsNullOrEmpty(path)) return suffixes;

            string[] parts = path.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries);

            int suffixesCount = (parts.Length - 1 < maxPathSuffixes - 1 ? parts.Length - 1 : maxPathSuffixes - 1);
            for (int i = 0; i <= suffixesCount; i++)
            {
                string suffix = (i == 0 ? string.Empty : string.Join("/", parts, 0, i));
                if (string.IsNullOrEmpty(suffix)) suffixes.Add("/");
                else suffixes.Add("/" + suffix + "/");
            }

            return suffixes;
        }

        /// <summary>
        /// Computes SHA256 hash of the specified string
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        public static string ComputeSha256Hash(string str)
        {
            byte[] hash = SHA256_INSTANCE.ComputeHash(DefaultEncoding.GetBytes(str));
            return ByteToHex(hash);
        }

        /// <summary>
        /// Converts string to int. If cannot -- returns defaultValue
        /// </summary>
        /// <param name="str"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static int ToInt(string str, int defaultValue)
        {
            try
            {
                if (string.IsNullOrEmpty(str)) return defaultValue;
                return int.Parse(str.Trim());
            }
            catch
            {
                return defaultValue;
            }
        }

        /// <summary>
        /// Converts string to int. If cannot -- returns -1
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        public static int ToInt(string str)
        {
            return ToInt(str, -1);
        }

        /// <summary>
        /// Converts byte array to int (using 4 bytes starting from "start" index of the specified array).
        /// Uses BitConverter and incapsulates IsLittleEndian check.
        /// </summary>
        /// <param name="bytes"></param>
        /// <param name="start"></param>
        /// <returns></returns>
        public static int ToInt(byte[] bytes, int start)
        {
            var data = new byte[4];
            Array.Copy(bytes, start, data, 0, 4);
            if (BitConverter.IsLittleEndian) Array.Reverse(data);
            return BitConverter.ToInt32(data, 0);
        }

        /// <summary>
        /// Reads "bytesToRead" bytes from the input stream
        /// </summary>
        /// <param name="input"></param>
        /// <param name="bytesToRead"></param>
        /// <returns></returns>
        /// <exception cref="IOException">When cannot read specified bytes count</exception>
        public static byte[] Read(Stream input, int bytesToRead)
        {
            var buffer = new byte[bytesToRead];
            int bytesRead = 0;
            int toRead = bytesToRead;
            while (toRead > 0)
            {
                int read = input.Read(buffer, bytesRead, toRead);
                bytesRead += read;
                toRead -= read;
                if (read <= 0) break;
            }

            return buffer;
        }

        /// <summary>
        /// Reads line from the input stream (until LF symbol)
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public static byte[] ReadLineBytes(Stream input)
        {
            using (var output = new MemoryStream())
            {
                int byteRead = input.ReadByte();
                while (byteRead != -1)
                {
                    switch (byteRead)
                    {
                        case LF:
                            return output.ToArray();
                        default:
                            output.WriteByte((byte) byteRead);
                            break;
                    }

                    byteRead = input.ReadByte();
                }

                return output.ToArray();
            }
        }

        /// <summary>
        /// Reads line from the input stream (until LF symbol).
        /// Returns it in DefaultEncoding.
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public static string ReadLine(Stream input)
        {
            return DefaultEncoding.GetString(ReadLineBytes(input));
        }

        /// <summary>
        /// Writes a text to the stream using specified encoding
        /// </summary>
        /// <param name="output"></param>
        /// <param name="text"></param>
        /// <param name="encoding"></param>
        public static void Write(Stream output, string text, Encoding encoding)
        {
            byte[] bytes = encoding.GetBytes(text);
            output.Write(bytes, 0, bytes.Length);
        }

        /// <summary>
        /// Reads stream to end
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public static byte[] ReadToEnd(Stream input)
        {
            using (var output = new MemoryStream())
            {
                Copy(input, output);
                return output.ToArray();
            }
        }

        /// <summary>
        /// Copies bytes from input stream to the output stream
        /// </summary>
        /// <param name="input"></param>
        /// <param name="output"></param>
        public static void Copy(Stream input, Stream output)
        {
            Copy(input, output, -1);
        }

        /// <summary>
        /// Copies bytes from input stream to the output stream,
        /// but not more then "contentLength". (If contentLength is "-1", copies everything).
        /// </summary>
        /// <param name="input"></param>
        /// <param name="output"></param>
        /// <param name="contentLength"></param>
        public static void Copy(Stream input, Stream output, long contentLength)
        {
            // doing nothing for content-length == 0
            if (contentLength == 0) return;

            int bufferSize = contentLength == -1
                                 ? BUFFER_SIZE
                                 : (BUFFER_SIZE > contentLength ? (int) contentLength : BUFFER_SIZE);
            var buffer = new byte[bufferSize];
            long bytesRead = 0;

            while (true)
            {
                int read = input.Read(buffer, 0, bufferSize);
                if (read <= 0) return;
                output.Write(buffer, 0, read);
                output.Flush();
                if (contentLength != -1)
                {
                    bytesRead += read;
                    if (bytesRead == contentLength) return;
                    if (contentLength - bytesRead < bufferSize)
                    {
                        bufferSize = (int) (contentLength - bytesRead);
                    }
                }
            }
        }

        /// <summary>
        /// Very fast bytes to hex convertation
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        public static string ByteToHex(byte[] data)
        {
            var lookup = new[] {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
            int i = 0, p = 0, l = data.Length;
            var c = new char[l*2];
            while (i < l)
            {
                byte d = data[i++];
                c[p++] = lookup[d/0x10];
                c[p++] = lookup[d%0x10];
            }
            return new string(c, 0, c.Length);
        }

        /// <summary>
        /// Verty fast hex to bytes convertation
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        public static byte[] HexToByte(string str)
        {
            var b = new byte[str.Length/2];
            int length = str.Length;
            for (int y = 0, x = 0; x < length; y++, x++)
            {
                var c1 = (byte) str[x];
                if (c1 > 0x60) c1 -= 0x57;
                else if (c1 > 0x40) c1 -= 0x37;
                else c1 -= 0x30;
                var c2 = (byte) str[++x];
                if (c2 > 0x60) c2 -= 0x57;
                else if (c2 > 0x40) c2 -= 0x37;
                else c2 -= 0x30;
                b[y] = (byte) ((c1 << 4) + c2);
            }
            return b;
        }
    }
}